/*************************************************************************
 * Copyright 2008 David Almilli
 *  
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 *  
 *     http://www.apache.org/licenses/LICENSE-2.0 
 *      
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 **************************************************************************/
package com.almilli.catnip.web;

import com.almilli.catnip.beacon.TiVoBeacon;
import com.almilli.catnip.logging.Log;
import com.almilli.catnip.logging.LogFactory;
import com.tivo.hme.interfaces.IApplication;
import com.tivo.hme.interfaces.IArgumentList;
import com.tivo.hme.interfaces.IFactory;
import com.tivo.hme.interfaces.IListener;
import com.tivo.hme.interfaces.ILogger;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.SocketException;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HmeServlet extends HttpServlet implements IListener, ILogger {
	private static final long serialVersionUID = 628820441674755100L;

    private static final Log log = LogFactory.getLog(HmeServlet.class);
	
    protected String argList;
    protected String appPath;
    protected String path;
    protected String appClassName;
    protected String factoryClassName;
    protected IFactory factory;

	@SuppressWarnings("unchecked")
    public void init(ServletConfig config) throws ServletException {
		super.init(config);

		appClassName = getInitParameter("ApplicationClass");
		if (appClassName == null) {
			throw new ServletException("You must specify an ApplicationClass parameter.");
		}
        factoryClassName = getInitParameter("FactoryClass");

        path = getInitParameter("Path");
        if (path == null) {
            throw new ServletException("You must specify a Path parameter that matches the servlet-mapping.");
        }

        argList = getInitParameter("ArgumentList");
        
        initFactory();
	}
	
	protected void initFactory() throws ServletException {
        factory = createFactory();
        String contextPath = getContextPath();
        appPath = contextPath + path;
        factory.setAppName(appPath);
        try {
            TiVoBeacon beacon = (TiVoBeacon)new InitialContext().lookup("java:comp/env/TiVoBeacon");
            beacon.register(factory);
            
        } catch (NamingException e) {
            if (log.isWarnEnabled()) {
                log.warn("Cannot find TiVoBeacon for " + appPath + 
                        ".  Auto discovery will not be enabled for this application.  You " +
                        "may be missing a resource definition for \"TiVoBeacon\"");
            }
        }
	}
	
	private String getContextPath() {
		ServletContext context = getServletContext();
		try {
			Method method = context.getClass().getMethod("getContextPath");
			return (String)method.invoke(context);
		} catch (Exception e) {
		}
		return null;
	}
	
	private IFactory createFactory() throws ServletException {
        IFactory factory = null;
        
		//load the factory for the application
        ClassLoader loader = Thread.currentThread().getContextClassLoader();

        if (factoryClassName != null) {
            //they specified a factory class, use that as an override
            try {
                Class<?> factoryClass = loader.loadClass(factoryClassName);
                if (!IFactory.class.isAssignableFrom(factoryClass)) {
                    if (log.isErrorEnabled()) {
                        log.error("Factory class does not implement IFactory: " + factoryClassName);
                    }
                }
                factory = (IFactory)factoryClass.newInstance();
                factory.initFactory(appClassName, loader, new ArgumentList(argList));
            } catch (Exception e) {
                if (log.isErrorEnabled()) {
                    log.error("Cannot load factory  class " + factoryClassName);
                }
                throw new ServletException("Cannot load factory class " + factoryClassName, e);
            }
        }
        
        if (factory == null) {
            //next try the lookup methods outlined by the HME docs
            try {
                //the sdk says you can "override" the static method so we need to use
                //reflection to call the getAppFactory method
                Class<?> cls = loader.loadClass(appClassName);
                Method method = cls.getMethod("getAppFactory", 
                    new Class[] {String.class, ClassLoader.class, IArgumentList.class });
                
                int mods = method.getModifiers();
                if (IFactory.class.isAssignableFrom(method.getReturnType()) && 
                        Modifier.isStatic(mods) && Modifier.isPublic(mods)) {
                    //it matches the signature, create the factory
                    factory = (IFactory)method.invoke(null, 
                            new Object[] { appClassName, loader, new ArgumentList(argList)});
                } else {
                    if (log.isWarnEnabled()) {
                        log.warn("The factory method does not match the signature: " +
                                "public static IFactory getAppFactory(String, ClassLoader, IArgumentList)");
                    }
                }
                
            } catch (Exception e) {
                if (log.isErrorEnabled()) {
                    log.error("Cannot load factory for application " + appClassName);
                }
                throw new ServletException("Cannot load factory for application " + appClassName, e);
            }
        }
        
		if (factory == null) {
            if (log.isErrorEnabled()) {
                log.error("Cannot find factory for application " + appClassName);
            }
			throw new ServletException("Cannot find factory for application " + appClassName);
		}
		factory.setListener(this);
		return factory;
	}

    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        String uri = request.getRequestURI();
        if (!uri.startsWith(appPath)) {
            if (log.isWarnEnabled()) {
                log.warn("Attempted to access a uri not under the base path.  Make sure your " +
                        "servlet-mapping matches your Path parameter.  (ex. /myapp/* for the " +
                        "mapping and /myapp/ for the Path)");
            }
            response.sendError(404);
            return;
        }
        if (uri.equals(appPath)) {
            eventStream(request, response);
        } else {
            fetchAsset(uri, request, response);
        }
    }

    protected void fetchAsset(String uri, HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        if (request.getQueryString() != null) {
            //the uri has a query string on it
            uri += "?" + request.getQueryString();
        }
        
        if (log.isInfoEnabled()) {
            log.info("Fetching asset " + uri);
        }
        try {
            //get the asset stream and write it to the response
            InputStream assetStr = factory.fetchAsset(new HmeHttpRequest(uri, request, response));
            try {
                OutputStream out = response.getOutputStream();
                byte[] data = new byte[4096];
                for (int count = assetStr.read(data); count != -1; count = assetStr.read(data)) {
                    out.write(data, 0, count);
                }
            } finally {
                assetStr.close();
            }

        } catch (IOException e) {
            log.info("Cannot fetch asset " + request.getRequestURI(), e);
        }
    }
    

    protected void eventStream(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
		log.debug("Meow.");
        
        Cookie idCookie = null;
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie c : cookies) {
                if (c.getName().equals("id")) {
                    idCookie = c;
                    break;
                }
            }
        }
        
        if (idCookie == null) {
            //make up a new cookie
            idCookie = new Cookie("id", getRandomId());
            idCookie.setMaxAge(630720000);  //20 years
            response.addCookie(idCookie);
        }
        
		response.setContentType("application/x-hme");
		response.flushBuffer();
		
		HmeContext context = new HmeContext(factory, request, response);
		context.setLogger(this);
		IApplication app = factory.createApplication(context);
        try {
        	log.info("Receiver connected.");
            app.open(context);
            factory.addApplication(app);

            try {
            	//start reading application events
            	readEvents(app, context.getInputStream());
            	
            } catch (SocketException e) {
            	String msg = e.getMessage();
            	if (msg != null && msg.equals("Connection reset")) {
	            	log.info("Receiver disconnected.");
	            	
	            	//receiver disconnected
	            	if (log.isDebugEnabled()) {
	            		log.debug("Receiver disconnected.", e);
	            	}
            	} else {
                    log.error("An error occurred when reading HME events.", e);
            	}
            }
            
        } catch (Exception e) {
            log.error("Cannot open application.", e);
            
        } finally {
            app.close();
        }
	}

    /**
     * Get some random bytes.
     */
    public static String getRandomId()
    {
        StringBuilder sb = new StringBuilder();
        for (int i=0; i < 32; i++) {
            sb.append(Integer.toHexString((int)(16*Math.random())));
        }
        return sb.toString();
    }

    protected void readEvents(IApplication app, PushbackInputStream in) throws IOException {
		try {
			boolean done = false;

			while (!done) {
				
				//wait until there is a chunk to read so we don't block inside a synchronized block
				int b = in.read();
				in.unread(b);

				synchronized (app) {
					// handle the chunk
					done = !app.handleChunk(in);
				}
			}
		} finally {
			getLogger().log(ILogger.LOG_NOTICE, "connection to receiver closed");
		}
	}

	public ILogger getLogger() {
		return this;
	}

	public void remove(IFactory factory) {
	}

	public void log(int priority, String s) {
		
		switch (priority) {
		case ILogger.LOG_DEBUG:
			s = "DEBUG: " + s;
			break;
		case ILogger.LOG_WARNING:
			s = "WARNING: " + s;
			break;
		case ILogger.LOG_NOTICE:
			s = "NOTICE: " + s;
			break;
		default:
		case ILogger.LOG_INFO:
			s = "INFO: " + s;
			break;
		}
		log(s);
	}

}
